52. 数据框拼接

为什么需要数据框拼接?

在真实的数据分析项目中,数据往往分散在多个来源中:

  • 行情数据:来自交易所的实时价格、成交量
  • 财务数据:来自公司财报的资产负债表、利润表
  • 宏观数据:来自统计部门的 GDP、CPI、利率
  • 情绪数据:来自新闻舆情、社交媒体的投资者情绪

将不同来源的数据整合在一起,是日常数据分析的核心挑战。

数据整合面临的核心问题

问题 说明
粒度不匹配 日频行情 vs 季频财务
时间对齐 不同市场的交易日历不同
键值识别 如何正确匹配同一公司的不同数据源?
重复数据 同一指标来自多个提供商
性能瓶颈 大规模数据集的合并操作极其耗时

本章学习目标

  • 理解 concatmergejoin 的本质区别与应用场景
  • 掌握不同类型连接(inner / left / right / outer)的含义
  • 学习金融时间序列数据的对齐与聚合技术
  • 了解大数据环境下的性能优化策略

数据拼接的两种方向

垂直拼接与水平拼接对比 左侧展示垂直拼接(行向堆叠),右侧展示水平拼接(列向合并) 垂直拼接(行向堆叠) D₁ (n 行) D₂ (m 行) 结果 (n+m 行) 水平拼接(列向合并) D₁ D₂ 结果(列数增加)

垂直拼接的数学表达

设两个数据矩阵 \(D_1\)\(n\) 行)和 \(D_2\)\(m\) 行),垂直拼接结果为:

\[ D_{\text{concat}} = \begin{bmatrix} D_1 \\ D_2 \end{bmatrix} \]

前提条件:两个数据集必须具有相同的列结构(相同列名和数据类型)。

水平拼接的数学表达

将多个数据集沿列方向(横向)合并,增加变量数量:

\[ D_{\text{merge}} = [D_1 \mid D_2] \]

前提条件:两个数据集必须具有相同的行数或可通过键值对齐。

关系代数基础

Pandas 的 merge 操作基于关系代数中的连接(Join)运算:

\[ R \bowtie_{\theta} S = \{ (r, s) \in R \times S \mid \theta(r, s) \} \]

  • \(R, S\):两个关系(数据表)
  • \(\bowtie\):连接运算符
  • \(\theta\):连接条件(通常是键值相等)

⭐ 平台任务1:从 Excel 导入两个 Sheet 的数据

Listing 1
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

price_JantoMar = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)#从外部导入Sheet1的5只股票信息链接为https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_JantoMar.head()) #查看前五行数据

print(price_JantoMar.tail()) #查看后五行数据

price_AprtoJui = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)##从外部导入Sheet2的5只股票信息链接为https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_AprtoJui.head()) #查看前五行数据

print(price_AprtoJui.tail()) #查看后五行数据
             中国移动   中国电信   中国人寿
日期                             
2019-01-02  47.51  50.60  10.44
2019-01-03  47.39  49.53  10.09
2019-01-04  49.23  50.44  10.55
2019-01-07  49.91  51.08  10.59
2019-01-08  50.33  51.36  10.72
             中国移动   中国电信   中国人寿
日期                             
2019-07-25  43.41  46.17  13.08
2019-07-26  43.48  45.58  13.08
2019-07-29  43.29  45.39  13.00
2019-07-30  42.85  45.00  12.86
2019-07-31  42.60  44.74  12.74
            中国铝业  中国海洋石油
日期                      
2019-01-02  7.88  150.00
2019-01-03  7.63  147.59
2019-01-04  7.98  155.74
2019-01-07  8.15  157.99
2019-01-08  8.48  161.07
            中国铝业  中国海洋石油
日期                      
2019-07-25  8.21  167.80
2019-07-26  8.29  166.82
2019-07-29  8.30  167.54
2019-07-30  8.21  167.00
2019-07-31  8.05  165.33

⭐ 平台任务2:使用 concat 按行拼接

Listing 2
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

# 从Excel文件读取数据存入price_JantoMar
price_JantoMar = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)

# 从Excel文件读取数据存入price_AprtoJui
price_AprtoJui = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)

price_JantoJul = pd.concat([price_JantoMar,price_AprtoJui],axis=0) #使用concat函数按行拼接

print(price_JantoJul.head())  #前五行数据

print(price_JantoJul.tail())  #后五行数据
             中国移动   中国电信   中国人寿  中国铝业  中国海洋石油
日期                                           
2019-01-02  47.51  50.60  10.44   NaN     NaN
2019-01-03  47.39  49.53  10.09   NaN     NaN
2019-01-04  49.23  50.44  10.55   NaN     NaN
2019-01-07  49.91  51.08  10.59   NaN     NaN
2019-01-08  50.33  51.36  10.72   NaN     NaN
            中国移动  中国电信  中国人寿  中国铝业  中国海洋石油
日期                                        
2019-07-25   NaN   NaN   NaN  8.21  167.80
2019-07-26   NaN   NaN   NaN  8.29  166.82
2019-07-29   NaN   NaN   NaN  8.30  167.54
2019-07-30   NaN   NaN   NaN  8.21  167.00
2019-07-31   NaN   NaN   NaN  8.05  165.33

⭐ 平台任务3:导入不同 Sheet 的股票数据

Listing 3
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

price_3stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据Sheet1 链接https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_3stocks.head()) #查看前五行数据

print(price_3stocks.tail()) #查看后五行数据

price_2stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0) #导入数据Sheet1 链接https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_2stocks.head()) #查看前五行数据

print(price_2stocks.tail()) #查看后五行数据 
             中国移动   中国电信   中国人寿
日期                             
2019-01-02  47.51  50.60  10.44
2019-01-03  47.39  49.53  10.09
2019-01-04  49.23  50.44  10.55
2019-01-07  49.91  51.08  10.59
2019-01-08  50.33  51.36  10.72
             中国移动   中国电信   中国人寿
日期                             
2019-07-25  43.41  46.17  13.08
2019-07-26  43.48  45.58  13.08
2019-07-29  43.29  45.39  13.00
2019-07-30  42.85  45.00  12.86
2019-07-31  42.60  44.74  12.74
            中国铝业  中国海洋石油
日期                      
2019-01-02  7.88  150.00
2019-01-03  7.63  147.59
2019-01-04  7.98  155.74
2019-01-07  8.15  157.99
2019-01-08  8.48  161.07
            中国铝业  中国海洋石油
日期                      
2019-07-25  8.21  167.80
2019-07-26  8.29  166.82
2019-07-29  8.30  167.54
2019-07-30  8.21  167.00
2019-07-31  8.05  165.33

⭐ 平台任务4:三种按列拼接方法对比

Listing 4
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

# 从Excel文件读取数据存入price_3stocks
price_3stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)

# 从Excel文件读取数据存入price_2stocks
price_2stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)

price_5stocks_concat = pd.concat([price_3stocks,price_2stocks],axis=1) #使用concat函数按列拼接

print(price_5stocks_concat.head()) #查看前五行数据

price_5stocks_merge = pd.merge(left=price_3stocks,right=price_2stocks,left_index=True,right_index=True) #使用merge函数按列拼接

print(price_5stocks_merge.head()) #查看前五行数据

price_5stocks_join = price_3stocks.join(price_2stocks,on="日期") #用join函数按列拼接

print(price_5stocks_join.head()) #查看前五行数据
             中国移动   中国电信   中国人寿  中国铝业  中国海洋石油
日期                                           
2019-01-02  47.51  50.60  10.44  7.88  150.00
2019-01-03  47.39  49.53  10.09  7.63  147.59
2019-01-04  49.23  50.44  10.55  7.98  155.74
2019-01-07  49.91  51.08  10.59  8.15  157.99
2019-01-08  50.33  51.36  10.72  8.48  161.07
             中国移动   中国电信   中国人寿  中国铝业  中国海洋石油
日期                                           
2019-01-02  47.51  50.60  10.44  7.88  150.00
2019-01-03  47.39  49.53  10.09  7.63  147.59
2019-01-04  49.23  50.44  10.55  7.98  155.74
2019-01-07  49.91  51.08  10.59  8.15  157.99
2019-01-08  50.33  51.36  10.72  8.48  161.07
             中国移动   中国电信   中国人寿  中国铝业  中国海洋石油
日期                                           
2019-01-02  47.51  50.60  10.44  7.88  150.00
2019-01-03  47.39  49.53  10.09  7.63  147.59
2019-01-04  49.23  50.44  10.55  7.98  155.74
2019-01-07  49.91  51.08  10.59  8.15  157.99
2019-01-08  50.33  51.36  10.72  8.48  161.07

concat 函数:垂直拼接的首选工具

pd.concat() 是 Pandas 中最通用的拼接函数,核心语法:

pd.concat([df1, df2], axis=0, ignore_index=True)
  • axis=0:垂直拼接(行方向堆叠)
  • axis=1:水平拼接(列方向合并)
  • ignore_index=True:重置索引,避免重复

concat 关键参数一览

参数 作用 默认值
objs 要拼接的对象列表 必需
axis 拼接方向(0=行,1=列) 0
ignore_index 忽略原索引,重新生成 False
keys 创建多级索引标识来源 None
join 列对齐方式(inner/outer) outer
sort 是否对列排序 True

示例:concat 垂直拼接

Listing 5
import pandas as pd
import numpy as np

# 贵州茅台(600519.SH)收益率数据
stock_a_returns = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=3),
    '股票代码': ['600519.SH'] * 3,
    '收益率': [0.02, -0.01, 0.03]
})

# 五粮液(000858.SZ)收益率数据
stock_b_returns = pd.DataFrame({
    '日期': pd.date_range('2024-01-04', periods=3),
    '股票代码': ['000858.SZ'] * 3,
    '收益率': [0.01, 0.02, -0.02]
})

# 垂直拼接,ignore_index=True 重置索引
all_returns = pd.concat(
    [stock_a_returns, stock_b_returns], ignore_index=True
)

print('股票A数据:')
print(stock_a_returns)
print('\n股票B数据:')
print(stock_b_returns)
print('\n拼接结果:')
print(all_returns)
股票A数据:
          日期       股票代码   收益率
0 2024-01-01  600519.SH  0.02
1 2024-01-02  600519.SH -0.01
2 2024-01-03  600519.SH  0.03

股票B数据:
          日期       股票代码   收益率
0 2024-01-04  000858.SZ  0.01
1 2024-01-05  000858.SZ  0.02
2 2024-01-06  000858.SZ -0.02

拼接结果:
          日期       股票代码   收益率
0 2024-01-01  600519.SH  0.02
1 2024-01-02  600519.SH -0.01
2 2024-01-03  600519.SH  0.03
3 2024-01-04  000858.SZ  0.01
4 2024-01-05  000858.SZ  0.02
5 2024-01-06  000858.SZ -0.02

concat 多级索引:追溯数据来源

Listing 6
# keys 参数为每个数据框分配标识键,创建多级索引
multi_index_returns = pd.concat(
    [stock_a_returns, stock_b_returns],
    keys=['股票A', '股票B'],
    names=['数据源', '行号']
)

print('多级索引结构:')
print(multi_index_returns)

print('\n索引信息:')
print(multi_index_returns.index)

# 选择特定来源的数据
print('\n仅选择股票A的数据:')
print(multi_index_returns.loc['股票A'])
多级索引结构:
               日期       股票代码   收益率
数据源 行号                            
股票A 0  2024-01-01  600519.SH  0.02
    1  2024-01-02  600519.SH -0.01
    2  2024-01-03  600519.SH  0.03
股票B 0  2024-01-04  000858.SZ  0.01
    1  2024-01-05  000858.SZ  0.02
    2  2024-01-06  000858.SZ -0.02

索引信息:
MultiIndex([('股票A', 0),
            ('股票A', 1),
            ('股票A', 2),
            ('股票B', 0),
            ('股票B', 1),
            ('股票B', 2)],
           names=['数据源', '行号'])

仅选择股票A的数据:
           日期       股票代码   收益率
行号                            
0  2024-01-01  600519.SH  0.02
1  2024-01-02  600519.SH -0.01
2  2024-01-03  600519.SH  0.03

应用场景:多源数据质量检查时,可通过多级索引快速定位问题数据的来源。

merge 函数:基于键值的水平合并

pd.merge() 基于共同的键值列进行水平合并:

pd.merge(left, right, on='键值列', how='inner')
  • on:指定匹配的键值列
  • how:连接方式(inner / left / right / outer)

示例:merge 内连接

Listing 7
# 股票基本信息
stock_info = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH'],
    '股票名称': ['贵州茅台', '五粮液', '招商银行'],
    '行业': ['食品饮料', '食品饮料', '金融']
})

# 股票财务数据(注意:第3只是中国平安,不是招商银行)
financial_data = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '601318.SH'],
    'PE': [35.2, 25.8, 10.5],
    'PB': [12.5, 8.3, 1.2]
})

# 内连接:只保留两边都有的股票
inner_result = pd.merge(
    stock_info, financial_data, on='股票代码', how='inner'
)

print('股票基本信息:')
print(stock_info)
print('\n财务数据:')
print(financial_data)
print('\n内连接结果(只保留两边都有的股票):')
print(inner_result)
股票基本信息:
        股票代码  股票名称    行业
0  600519.SH  贵州茅台  食品饮料
1  000858.SZ   五粮液  食品饮料
2  600036.SH  招商银行    金融

财务数据:
        股票代码    PE    PB
0  600519.SH  35.2  12.5
1  000858.SZ  25.8   8.3
2  601318.SH  10.5   1.2

内连接结果(只保留两边都有的股票):
        股票代码  股票名称    行业    PE    PB
0  600519.SH  贵州茅台  食品饮料  35.2  12.5
1  000858.SZ   五粮液  食品饮料  25.8   8.3

内连接的数学含义

\[ R \bowtie S = \{ (r, s) \mid r[\text{key}] = s[\text{key}] \} \]

只有键值在两个数据集中都存在的行才会被保留。

  • 贵州茅台 ✅ 两边都有 → 保留
  • 五粮液 ✅ 两边都有 → 保留
  • 招商银行 ❌ 只在左表 → 丢弃
  • 中国平安 ❌ 只在右表 → 丢弃

四种连接类型对比

Listing 8
# 左连接:保留左表所有行
left_result = pd.merge(
    stock_info, financial_data, on='股票代码', how='left'
)

# 右连接:保留右表所有行
right_result = pd.merge(
    stock_info, financial_data, on='股票代码', how='right'
)

# 外连接:保留所有行
outer_result = pd.merge(
    stock_info, financial_data, on='股票代码', how='outer'
)

print('左连接结果(保留左边所有股票):')
print(left_result)
print('\n右连接结果(保留右边所有股票):')
print(right_result)
print('\n外连接结果(保留所有股票):')
print(outer_result)
左连接结果(保留左边所有股票):
        股票代码  股票名称    行业    PE    PB
0  600519.SH  贵州茅台  食品饮料  35.2  12.5
1  000858.SZ   五粮液  食品饮料  25.8   8.3
2  600036.SH  招商银行    金融   NaN   NaN

右连接结果(保留右边所有股票):
        股票代码  股票名称    行业    PE    PB
0  600519.SH  贵州茅台  食品饮料  35.2  12.5
1  000858.SZ   五粮液  食品饮料  25.8   8.3
2  601318.SH   NaN   NaN  10.5   1.2

外连接结果(保留所有股票):
        股票代码  股票名称    行业    PE    PB
0  000858.SZ   五粮液  食品饮料  25.8   8.3
1  600036.SH  招商银行    金融   NaN   NaN
2  600519.SH  贵州茅台  食品饮料  35.2  12.5
3  601318.SH   NaN   NaN  10.5   1.2

连接类型的决策指南

场景 推荐类型 理由
主数据表匹配补充信息 left 保证主表数据不丢失
数据源可靠性相同 inner 只保留两边都有的高质量数据
整合多个不完整来源 outer 最大化信息利用
连接类型决策树 根据业务需求选择合适的连接类型的决策流程图 需要保留左边所有数据? LEFT JOIN 需要保留右边所有数据? RIGHT JOIN 需要保留所有数据? OUTER JOIN INNER JOIN

join 方法:基于索引的便捷合并

df.join()merge 的特例,专门用于基于索引的合并:

df1.join(df2, how='left')
  • 使用 join:数据已经以键值为索引,代码更简洁
  • 使用 merge:需要基于列进行连接,或需要更复杂的连接条件

示例:join 基于索引合并

Listing 9
# 收益率数据,股票代码为索引
returns = pd.DataFrame({
    '日收益率': [0.02, 0.01, -0.01]
}, index=['600519.SH', '000858.SZ', '600036.SH'])

# 波动率数据,股票代码为索引(第3只不同)
volatility = pd.DataFrame({
    '年化波动率': [0.25, 0.30, 0.20]
}, index=['600519.SH', '000858.SZ', '601318.SH'])

# 基于索引左连接
joined_data = returns.join(volatility, how='left')

print('收益率数据:')
print(returns)
print('\n波动率数据:')
print(volatility)
print('\n基于索引的左连接结果:')
print(joined_data)
收益率数据:
           日收益率
600519.SH  0.02
000858.SZ  0.01
600036.SH -0.01

波动率数据:
           年化波动率
600519.SH   0.25
000858.SZ   0.30
601318.SH   0.20

基于索引的左连接结果:
           日收益率  年化波动率
600519.SH  0.02   0.25
000858.SZ  0.01   0.30
600036.SH -0.01    NaN

三种拼接方法总结对比

方法 适用场景 拼接方向 匹配方式
pd.concat() 结构相同的数据堆叠 纵向/横向 索引对齐
pd.merge() 基于键值列的关联 横向 列值匹配
df.join() 基于索引的关联 横向 索引匹配

金融实战:多源数据整合

任务:整合股票基本信息、行情数据、财务指标,构建完整的分析数据集。

Listing 10
import pandas as pd

# 数据源1:股票基本信息(主表)
stock_basic = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH'],
    '股票名称': ['贵州茅台', '五粮液', '招商银行', '中国平安'],
    '上市日期': ['2001-08-27', '1998-04-27', '2002-04-09', '2007-03-01'],
    '行业': ['食品饮料', '食品饮料', '金融', '金融']
})

# 数据源2:日行情数据(缺少中国平安)
daily_quote = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH'],
    '收盘价': [1850.00, 158.50, 32.80],
    '涨跌幅': [1.5, -0.8, 0.5]
})

# 数据源3:财务指标(缺少招商银行)
financial_metrics = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '601318.SH'],
    'ROE': [25.8, 22.3, 15.6],
    '负债率': [18.5, 30.2, 92.5]
})

金融实战:逐步整合数据

Listing 11
# 步骤1:基本信息 + 行情数据(左连接)
step1 = pd.merge(
    stock_basic, daily_quote,
    on='股票代码', how='left', indicator=True
)
print('步骤1:基本信息 + 行情数据')
print(step1)

# 步骤2:继续加入财务指标
final_data = pd.merge(
    step1, financial_metrics,
    on='股票代码', how='left', suffixes=('', '_财务')
)
print('\n最终整合结果:')
print(final_data)

# 数据完整性分析
print('\n缺失值统计:')
print(final_data.isna().sum())
步骤1:基本信息 + 行情数据
        股票代码  股票名称        上市日期    行业     收盘价  涨跌幅     _merge
0  600519.SH  贵州茅台  2001-08-27  食品饮料  1850.0  1.5       both
1  000858.SZ   五粮液  1998-04-27  食品饮料   158.5 -0.8       both
2  600036.SH  招商银行  2002-04-09    金融    32.8  0.5       both
3  601318.SH  中国平安  2007-03-01    金融     NaN  NaN  left_only

最终整合结果:
        股票代码  股票名称        上市日期    行业     收盘价  涨跌幅     _merge   ROE   负债率
0  600519.SH  贵州茅台  2001-08-27  食品饮料  1850.0  1.5       both  25.8  18.5
1  000858.SZ   五粮液  1998-04-27  食品饮料   158.5 -0.8       both  22.3  30.2
2  600036.SH  招商银行  2002-04-09    金融    32.8  0.5       both   NaN   NaN
3  601318.SH  中国平安  2007-03-01    金融     NaN  NaN  left_only  15.6  92.5

缺失值统计:
股票代码      0
股票名称      0
上市日期      0
行业        0
收盘价       1
涨跌幅       1
_merge    0
ROE       1
负债率       1
dtype: int64

数据整合的关键决策

  • 主表选择:以股票基本信息为主表,使用 left join 确保不丢失股票
  • 来源追踪:使用 indicator=True 标识每条数据是否成功匹配
  • 列名冲突:使用 suffixes 参数处理重名列
  • 缺失值含义:财务指标缺失可能意味着该股票尚未发布财报

性能优化:大数据集合并

Listing 12
import pandas as pd
import numpy as np
import time

n_stocks = 5000
n_dates = 1000

# 生成大规模测试数据(500万行行情 + 5000行财务)
quotes = pd.DataFrame({
    '股票代码': np.repeat(
        [f'{i:06d}.SH' for i in range(n_stocks)], n_dates
    ),
    '日期': list(pd.date_range('2020-01-01', periods=n_dates)) * n_stocks,
    '收盘价': np.random.uniform(10, 100, n_stocks * n_dates)
})

financials = pd.DataFrame({
    '股票代码': [f'{i:06d}.SH' for i in range(n_stocks)],
    'ROE': np.random.uniform(5, 30, n_stocks),
    '市值': np.random.uniform(50, 5000, n_stocks)
})

# 方法1:未优化
start_time = time.time()
result_slow = pd.merge(quotes, financials, on='股票代码')
slow_time = time.time() - start_time

# 方法2:将键值转为 category 类型
quotes_opt = quotes.copy()
quotes_opt['股票代码'] = quotes_opt['股票代码'].astype('category')
financials_opt = financials.copy()
financials_opt['股票代码'] = financials_opt['股票代码'].astype('category')

start_time = time.time()
result_fast = pd.merge(quotes_opt, financials_opt, on='股票代码')
fast_time = time.time() - start_time

print(f'未优化合并时间: {slow_time:.2f}秒')
print(f'优化后合并时间: {fast_time:.2f}秒')
print(f'性能提升: {slow_time/fast_time:.1f}倍')
未优化合并时间: 0.86秒
优化后合并时间: 0.12秒
性能提升: 7.3倍

性能优化清单

优化策略 说明
键值类型优化 将字符串键值转换为 category 类型
索引优化 对键值列建立索引(df.set_index()
避免重复 合并前检查并删除重复数据
分块处理 超大文件考虑分块读取和合并
使用 Dask 超出内存容量时使用并行计算框架

高级主题:多键连接

Listing 13
# 行情数据
quotes = pd.DataFrame({
    '股票代码': ['600519.SH', '600519.SH', '000858.SZ'],
    '日期': ['2024-01-01', '2024-01-02', '2024-01-01'],
    '收盘价': [1850.0, 1870.0, 158.5]
})

# 财务数据
financals = pd.DataFrame({
    '股票代码': ['600519.SH', '600519.SH', '000858.SZ'],
    '日期': ['2024-01-01', '2024-01-02', '2024-01-01'],
    'PE': [35.2, 35.8, 25.8]
})

# 基于股票代码 + 日期两个键进行合并
merged = pd.merge(
    quotes, financals,
    on=['股票代码', '日期'], how='inner'
)

print('多键连接结果:')
print(merged)
多键连接结果:
        股票代码          日期     收盘价    PE
0  600519.SH  2024-01-01  1850.0  35.2
1  600519.SH  2024-01-02  1870.0  35.8
2  000858.SZ  2024-01-01   158.5  25.8

多键连接的数学含义

\[ R \bowtie_{k_1, k_2} S = \{ (r, s) \mid r[k_1] = s[k_1] \land r[k_2] = s[k_2] \} \]

只有所有指定的键值都匹配时,两行数据才会被连接。

应用场景:确保分析的是同一股票在同一天的完整数据,避免错误地将不同日期的数据拼接在一起。

本章总结

  • pd.concat()堆叠数据,适合结构相同的数据框沿行/列方向拼接
  • pd.merge()关联数据,基于键值列进行水平合并,支持四种连接类型
  • df.join()索引合并,merge 的简化版,适合索引已设好的场景
  • 连接类型选择:根据业务需求决定使用 inner / left / right / outer
  • 性能优化:键值转为 category 类型可显著提升大数据集合并速度